﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Mono.Cecil;
using QuickGraph;
using cilada.engine.Graph;
using System.Diagnostics;
using Mono.Cecil.Cil;

namespace cilada.engine
{
    public class TypeMetadataGraphBuilder
    {
        #region Properties
        public bool DetailedAnalysis { get; set; }
        public bool Verbose { get; set; }
        public List<string> Errors { get; set; }

        public QuickGraphAdapter<IMember, TaggedEdge<IMember, EdgeType>> AssemblyGraph
        {
            get;
            set;
        }
        #endregion

        #region Constructors
        public TypeMetadataGraphBuilder()
        {
            this.Verbose = false;
            this.Errors = new List<string>();
            this.DetailedAnalysis = true;
            this.AssemblyGraph = new QuickGraphAdapter<IMember, TaggedEdge<IMember, EdgeType>>();
        }
        #endregion

        #region Instance Methods
        #region Public
        public void PrintErrors()
        {
            if (this.Errors.Count > 0)
            {
                Console.WriteLine("Errors:");
                foreach (string e in this.Errors)
                {
                    Console.WriteLine("  " + e);
                }
            }
        }

        public void AnalyseAssemblies(IDictionary<string, string> assemblyNamesAndPaths, IEnumerable<string> assemblyDirectories)
        {
            if (!AddDirsToMonoCecilGlobalAssemblyResolver(assemblyDirectories) && this.Verbose)
            {
                Console.WriteLine("Could not add assembly dirs to the Mono.Cecil GlobalAssemblyResolver. This may cause errors.");
            }

            // foreach assembly
            foreach (KeyValuePair<string, string> e in assemblyNamesAndPaths)
            {
                AnalyseAssembly(e.Key, e.Value);
            }

            RemoveDirsFromMonoCecilGlobalAssemblyResolver(assemblyDirectories);
        }

        public void AnalyseAssembly(string assemblyName, string path)
        {
            AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(path);
            // foreach module
            foreach (ModuleDefinition md in assembly.Modules)
            {
                // TODO is there any case where this module should not be skipped?
                // what is actually in here?
                if (!"<Module>".Equals(md.Name))
                {
                    if (this.Verbose) Console.WriteLine("Module => " + md.Name);

                    foreach (TypeDefinition td in md.Types)
                    {
                        // TODO add an exclusion for the "<Module>" type ?
                        AnalyseType(td, assemblyName);
                    }
                }
            }
        }
        #endregion

        #region Protected / Private
        protected void AnalyseType(TypeDefinition td, string container)
        {
            if (td == null) return;

            if (this.Verbose) Console.WriteLine("\tType=" + td.FullName);

            TypeVertex tv = td.ToTypeVertex(container);
            AssemblyGraph.AddVertex(tv);

            // get base type
            if (td.BaseType != null)
            {
                ResolveTypeAndAddEdge(tv, td.BaseType, EdgeType.extends);
            }

            // foreach interface
            foreach (TypeReference inter in td.Interfaces)
            {
                ResolveTypeAndAddEdge(tv, inter, EdgeType.implements);
            }

            TypeInfoWrapper tif = new TypeInfoWrapper(td, container, tv);

            // get generic type restrictions, if any
            if (td.HasGenericParameters)
            {
                ExtractTypeConstraints(tif);
            }

            // foreach nested type
            foreach (TypeDefinition ntd in td.NestedTypes)
            {
                AnalyseType(ntd, container);
            }

            // foreach property
            if (this.Verbose) Console.WriteLine("\t  Properties:");
            foreach (PropertyDefinition pd in td.Properties)
            {
                AnalyseProperty(pd, tif);
            }

            if (this.Verbose) Console.WriteLine("\t  Methods:");
            // foreach method
            foreach (MethodDefinition md in td.Methods)
            {
                AnalyseMethod(md, tif);
            }

            foreach (EventDefinition ed in td.Events)
            {
                AnalyseEvent(ed, tif);
            }

            if (this.Verbose) Console.WriteLine();
        }

        private void AnalyseMethod(MethodDefinition md, TypeInfoWrapper tif)
        {
            if (md == null) return;

            MethodVertex mv = md.ToMethodVertex();
            AssemblyGraph.AddVertex(mv);

            CreateTaggedEdgeAndAddToGraph(mv, tif.TypeVertex, EdgeType.belongsto);
            if (this.Verbose) Console.WriteLine("\t    " + md.Name);

            // get method return type
            if (!md.ReturnType.IsGenericParameter) // FIXME find out exactly how this behaves.
            {
                TypeDefinition returnType = null;
                if (md.ReturnType.TryResolveTypeReference(out returnType))
                {
                    // is this if statement needed?
                    if (md.ReturnType.IsGenericInstance)
                    {
                        ResolveGenericTypeParameters(tif, md.ReturnType);
                    }
                    CreateTaggedEdgeAndAddToGraph(mv, returnType.ToTypeVertex(), EdgeType.hasreturnType);
                }
                else
                {
                    this.Errors.Add("Could not resolve return type " + md.ReturnType.FullName + " for method " + md.FullName);
                }
            }

            // if the method is on a generic instance, and uses the types, try to resolve them.
            if (md.IsGenericInstance || md.HasGenericParameters)
            {
                ResolveGenericMethodParameters(tif, md as MethodReference);
            }

            // are these only the non generic params?
            // get method parameter types
            foreach (ParameterDefinition pd in md.Parameters)
            {
                if (!pd.ParameterType.IsGenericParameter)
                {
                    ResolveTypeAndAddEdge(tif.TypeVertex, pd.ParameterType, EdgeType.uses);
                }
            }
            // print out a list of parameters and any available type data for checking.

            // get types used in method body
            AnalyseMethodBody(mv, md, tif);
        }

        private void AnalyseProperty(PropertyDefinition pd, TypeInfoWrapper tif)
        {
            if (pd == null) return;

            AttributeVertex av = pd.ToAttributeVertex();
            AssemblyGraph.AddVertex(av);

            CreateTaggedEdgeAndAddToGraph(tif.TypeVertex, av, EdgeType.hasattribute);

            if (this.Verbose) Console.WriteLine("\t    " + pd.Name); // FIXME or use full name?

            // add the type, if it is known at compile time.
            if (!pd.PropertyType.IsGenericParameter) // FIXME will this ever be entered?
            {
                ResolveTypeAndAddEdge(tif.TypeVertex, pd.PropertyType, EdgeType.uses);
            }

            // get types used in get method
            AnalyseMethodBody(null, pd.GetMethod, tif);

            // get types used in set method
            AnalyseMethodBody(null, pd.SetMethod, tif);
        }

        private void AnalyseEvent(EventDefinition ed, TypeInfoWrapper tif)
        {
            // TODO - complete event analysis.
            ResolveTypeAndAddEdge(tif.TypeVertex, ed.EventType, EdgeType.uses);
        }

        private void AnalyseMethodBody(MethodVertex mv, MethodDefinition md, TypeInfoWrapper tif)
        {
            // what params are needed for this
            if (md == null) return;

            // FIXME is this step even neccessary? vertices should be compared by full name, which will be the same.
            if (mv == null)
            {
                mv = md.ToMethodVertex();
            }

            if (md.HasBody && md.Body != null)
            {
                SequencePoint currentSequencePoint = null;
                foreach (Instruction i in md.Body.Instructions)
                {
                    currentSequencePoint = i.SequencePoint ?? currentSequencePoint;

                    // find types.
                    if (i.Operand != null)
                    {
                        if (i.OpCode.OperandType == OperandType.InlineType)
                        {
                            TypeReference tr = i.Operand as TypeReference;

                            // FIXME what if tr is a generic type?
                            // how to tell the difference between a generic parameter that is defined in 
                            // the class, and one that is taken from the classes generic params?
                            // simply check if the parent type has any generic params?
                            EdgeType edgeType = (tif.TypeDefinition.HasGenericParameters &&
                                tr.HasGenericParameters && tr.IsGenericParameter)
                                ? EdgeType.guses : EdgeType.uses;

                            ResolveTypeAndAddEdge(tif.TypeVertex, tr, edgeType);
                        }
                        else
                        {
                            if (i.Operand is MethodReference)
                            {
                                MethodReference mr = (MethodReference)i.Operand;

                                // FIXME what if mr is a generic method?

                                TaggedEdge<IMember, EdgeType> e = new TaggedEdge<IMember, EdgeType>(md.ToMethodVertex(), mr.ToMethodVertex(), EdgeType.invokes);
                                AssemblyGraph.AddEdge(e);
                            }
                        }
                    }

                    // should also check InlineToken, re http://go-mono.com/forums/#nabble-td1551209
                }
            }
        }

        private void ExtractTypeConstraints(TypeInfoWrapper tif)
        {
            if (tif.TypeDefinition.HasGenericParameters)
            {
                foreach (GenericParameter gp in tif.TypeDefinition.GenericParameters)
                {
                    foreach (TypeReference c in gp.Constraints)
                    {
                        ResolveTypeAndAddEdge(tif.TypeVertex, c, EdgeType.uses);
                    }
                }
            }
        }

        private void ExtractMethodConstraints(MethodDefinition md)
        {
            // if method has generic params, may have constraints on them.
        }

        private void ResolveGenericTypeParameters(TypeInfoWrapper tif, TypeReference tr)
        {
            if (tr.IsGenericInstance)
            {
                GenericInstanceType gt = tr as GenericInstanceType;
                foreach (TypeReference ga in gt.GenericArguments)
                {
                    ResolveTypeAndAddEdge(tif.TypeVertex, ga, EdgeType.guses);

                    // dont need to find type constraints here, as they will be found by analysing the 
                    // type if it is in the set of assemblies being analysed, or will not be relevant if
                    // if it is not in this set.
                }
            }
        }

        private void ResolveGenericMethodParameters(TypeInfoWrapper tif, MethodReference md)
        {
            if (md.IsGenericInstance)
            {
                GenericInstanceMethod gm = md as GenericInstanceMethod;

                // resolve return type, if generic.
                if (md.ReturnType.IsGenericParameter)
                {
                    //gm.FullName;
                }

                foreach (TypeReference ga in gm.GenericArguments)
                {
                    ResolveTypeAndAddEdge(tif.TypeVertex, ga, EdgeType.guses);

                    GenericParameter gp = ga as GenericParameter;
                    if (gp != null && gp.HasConstraints)
                    {
                        foreach (TypeReference c in gp.Constraints)
                        {
                            ResolveTypeAndAddEdge(tif.TypeVertex, c, EdgeType.guses);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Attempts to resolve the given type reference, and if resolved, add an edge from the given member vertex to the type reference vertex, with the given edge type.
        /// This will only be 
        /// </summary>
        /// <param name="mv">The member vertex to use as the source of the edge.</param>
        /// <param name="tr">The type reference to resolve.</param>
        /// <param name="edgeType">The edge type that the edge should be tagged with.</param>
        /// <returns>True if the type reference was resolved and added, false otherwise.</returns>
        private bool ResolveTypeAndAddEdge(IMember mv, TypeReference tr, EdgeType edgeType)
        {
            TypeDefinition td = null;
            if (tr.TryResolveTypeReference(out td))
            {
                // FIXME is this if statement needed? Won't the vertex be automatically checked to see if it exists anyway?
                IMember vertex = null;
                if (AssemblyGraph.ContainsVertex(v => v.FullName.Equals(td.FullName), out vertex))
                {
                    CreateTaggedEdgeAndAddToGraph(mv, vertex, edgeType);
                }
                else
                {
                    CreateTaggedEdgeAndAddToGraph(mv, td.ToTypeVertex(), edgeType);
                }
                return true;
            }
            else
            {
                this.Errors.Add(String.Format("Could not resolve type reference {0}, so could not create edge of type {1} from source {2}", tr.FullName, edgeType, mv));
            }
            return false;
        }

        private void CreateTaggedEdgeAndAddToGraph(IMember source, IMember target, EdgeType edgeType)
        {
            TaggedEdge<IMember, EdgeType> e = new TaggedEdge<IMember, EdgeType>(source, target, edgeType);
            AssemblyGraph.AddEdge(e);
        }

        private bool AddDirsToMonoCecilGlobalAssemblyResolver(IEnumerable<string> assemblyDirectories)
        {
            // add paths to global assembly resolver

            bool isDAR = GlobalAssemblyResolver.Instance is DefaultAssemblyResolver;
            if (!isDAR) return false;

            DefaultAssemblyResolver globalResolver = (DefaultAssemblyResolver)GlobalAssemblyResolver.Instance;
            foreach (string dir in assemblyDirectories)
            {
                globalResolver.AddSearchDirectory(dir);
            }

            return true;
        }

        private bool RemoveDirsFromMonoCecilGlobalAssemblyResolver(IEnumerable<string> assemblyDirectories)
        {
            // add paths to global assembly resolver

            bool isDAR = GlobalAssemblyResolver.Instance is DefaultAssemblyResolver;
            if (!isDAR) return false;

            DefaultAssemblyResolver globalResolver = (DefaultAssemblyResolver)GlobalAssemblyResolver.Instance;
            foreach (string dir in assemblyDirectories)
            {
                globalResolver.RemoveSearchDirectory(dir);
            }

            return true;
        }
        #endregion
        #endregion
    }
}
